import { Diagnostic, DiagnosticMessage, errorMessages } from "../diagnostic";
import { getMapEntries, stringEqual } from "../../common/util";
import { MDMSerializerOptions, Mutable } from "../../common/types";
import { MetadataDiagnostic } from "./errors";
import * as t from "./types";
import {
    alignmentToString,
    bitAnd,
    booleanToString,
    borderStyleToString,
    controlTypeToString,
    cursorTypeToString,
    dataTypeToString,
    displayOrderToScript,
    displayOrderToXmlValue,
    elementAlignToString,
    elementTypeConstantsToTypeName,
    formateValueByDataType,
    getFactorType,
    imagePositionToString,
    infoTypeToString,
    isMDMClass,
    isMDMCompound,
    isMDMDocument,
    isMDMElement,
    isMDMElementInstance,
    isMDMElements,
    isMDMField,
    isMDMPage,
    isMDMVariable,
    isScriptElementType,
    mdmValueToXmlType,
    orientationToString,
    variableUsageFlagsToString,
    verticalAlignToString
} from "./util";
import {
    createXmlPropertyNode,
    domImpl,
    normalToHtmlEscapeCharater
} from "./xml";


const specialPropertyNames = [
    "DisplayOrder",
    "DisplayOrientation"
];

const reservedNameObj = {
    multiplier: "Multiplier",
    code: "Code",
};

const reservedName = new Map(getMapEntries(reservedNameObj));

interface LabeledObjectCallback {
    properties?: (e?: Element, resultElement?: Element) => void;
    styles?: (e?: Element, resultElement?: Element) => void;
    labelStyles?: (e?: Element, resultElement?: Element) => void;
    templates?: (e?: Element, resultElement?: Element) => void;
};

export default class Serializer implements t.Serializer {

    public readonly errors: Diagnostic[] = [];
    private options: MDMSerializerOptions;

    public constructor(options: MDMSerializerOptions) {
        this.options = options;
    }

    private error(diagnostic: DiagnosticMessage, range: { start: number, length: number }, ...messages: string[]): void;
    private error(diagnostic: DiagnosticMessage, elements: (Element | t.IMDMObject)[], ...messages: string[]): void;
    private error(diagnostic: DiagnosticMessage, arg1: (Element | t.IMDMObject)[] | { start: number, length: number }, ...messages: string[]) {
        let message = diagnostic.template;
        messages.forEach((v, i) => message = message.replace(`{${i.toString()}}`, v));
        let diag: Diagnostic = {
            flag: diagnostic.flag,
            code: diagnostic.code,
            start: -1,
            length: 0,
            message
        };
        if (Array.isArray(arg1)) {
            (diag as Mutable<MetadataDiagnostic>).elements = arg1;
        }
        else {
            (diag as Mutable<Diagnostic>).start = arg1.start;
            (diag as Mutable<Diagnostic>).length = arg1.length;
        }
        this.errors.push(diag);
    }

    private serializePropertyToString(property: t.Property, indentLevel = 0): string {
        let value = typeof property.value === "string"  ? `"${property.value}"` :
                    typeof property.value === "number"  ? property.value.toString() :
                    typeof property.value === "boolean" ? (property.value ? "true" : "false") : "null";
        return `${" ".repeat(indentLevel * this.options.indentSpaceCount)}${property.name} = ${value}`;
    }

    private serializePropertyToXml(dom: XMLDocument, property: t.Property): Element {
        const element = dom.createElement("property");
        element.setAttribute("name", property.name);
        let value: string | undefined;
        if (typeof property.value === "string") {
            value = normalToHtmlEscapeCharater(property.value);
        }
        else if (typeof property.value === "number") {
            value = property.value.toString();
        }
        else if (typeof property.value === "boolean") {
            value = booleanToString(property.value);
        }
        else if (property.value) {
            element.appendChild(this.serializePropertiesToXml(dom, property.value));
        }
        if (value) {
            element.setAttribute("value", value);
        }
        if (property.valueType) {
            element.setAttribute("type", property.valueType.toString());
        }
        element.setAttribute("context", property.context);
        if (property.dataSource) {
            element.setAttribute("ds", property.dataSource);
        }
        return element;
    }

    private serializePropertiesToString(properties: t.Property[], indentLevel = 0): string {
        const indent = " ".repeat(indentLevel * this.options.indentSpaceCount);
        const items: string[] = [];
        for (const item of properties) {
            if (specialPropertyNames.find(e => e === item.name) || !item.versioned) {
                continue;
            }
            items.push(`${this.options.lineFeedCharater}${this.serializePropertyToString(item, indentLevel + 1)}`);
        }
        return items.length > 0 ? `${this.options.lineFeedCharater}${indent}[${items.join(",")}${this.options.lineFeedCharater}${indent}]` : "";
    }

    private serializePropertiesToXml(dom: Document, properties: t.Properties, specifyTag?: string): Element {
        const element = dom.createElement(specifyTag ?? "properties");
        const unversioned: Element[] = [];
        const versioned: Element[] = [];
        for (const prop of properties) {
            for (const p of prop.value) {
                const ele = this.serializePropertyToXml(dom, p);
                if (!p.versioned) {
                    unversioned.push(ele);
                }
                else {
                    versioned.push(ele);
                }
            }
        }
        if (unversioned.length > 0) {
            const unversionedElement = dom.createElement("unversioned");
            unversioned.forEach(e => unversionedElement.appendChild(e));
            element.appendChild(unversionedElement);
        }
        versioned.forEach(e => element.appendChild(e));
        return element;
    }

    private serializeFontStyleToString(fontStyle: t.FontStyle, indentLevel = 0): string {
        const indent = " ".repeat(indentLevel * this.options.indentSpaceCount);
        const tab = " ".repeat(this.options.indentSpaceCount);
        const itemIndentSpace = `${this.options.lineFeedCharater}${indent}${tab}`;
        let items: string[] = [];
        if (fontStyle.family !== undefined) {
            items.push(`${itemIndentSpace}Family = "${fontStyle.family}"`);
        }
        if (fontStyle.size !== undefined) {
            items.push(`${itemIndentSpace}Size = ${fontStyle.size.toString()}`);
        }
        if (fontStyle.effects !== undefined) {
            items.push(`${itemIndentSpace}IsUnderline = ${((fontStyle.effects & t.FontStyleEffects.isUnderline) > 0 ? "true" : "false")}`);
            items.push(`${itemIndentSpace}IsItalic = ${((fontStyle.effects & t.FontStyleEffects.isItalic) > 0 ? "true" : "false")}`);
            items.push(`${itemIndentSpace}IsBold = ${((fontStyle.effects & t.FontStyleEffects.isBold) > 0 ? "true" : "false")}`);
            items.push(`${itemIndentSpace}IsStrikethrough = ${((fontStyle.effects & t.FontStyleEffects.isStrikethrough) > 0 ? "true" : "false")}`);
            items.push(`${itemIndentSpace}IsOverline = ${((fontStyle.effects & t.FontStyleEffects.isOverline) > 0 ? "true" : "false")}`);
            items.push(`${itemIndentSpace}IsBlink = ${((fontStyle.effects & t.FontStyleEffects.isBlink) > 0 ? "true" : "false")}`);
            items.push(`${itemIndentSpace}IsSuperscript = ${((fontStyle.effects & t.FontStyleEffects.isSuperscript) > 0 ? "true" : "false")}`);
            items.push(`${itemIndentSpace}IsSubscript = ${((fontStyle.effects & t.FontStyleEffects.isSubscript) > 0 ? "true" : "false")}`);
        }
        return `${indent}Font(${items.join(",")}${this.options.lineFeedCharater}${indent})`;
    }

    private serializeFontStyleToXml(dom: XMLDocument, fontStyle: t.FontStyle): Element {
        const element = dom.createElement("property");
        element.setAttribute("name", "Font");
        element.setAttribute("type", "9");
        element.setAttribute("context", fontStyle.context);
        const labelStyles = dom.createElement("labelstyles");
        if (fontStyle.family) {
            labelStyles.appendChild(createXmlPropertyNode(dom, "Family", fontStyle.family, "8", fontStyle.context));
        }
        if (fontStyle.effects !== undefined) {
            labelStyles.appendChild(createXmlPropertyNode(dom, "Effects", fontStyle.effects.toString(), "3", fontStyle.context));
        }
        if (fontStyle.size !== undefined) {
            labelStyles.appendChild(createXmlPropertyNode(dom, "Size", fontStyle.size.toString(), "3", fontStyle.context));
        }
        element.appendChild(labelStyles);
        return element;
    }

    private serializeCellStyleToString(cellStyle: t.CellStyle, indentLevel = 0): string {
        const indent = " ".repeat(indentLevel * this.options.indentSpaceCount);
        const itemIndentSpace = `${this.options.lineFeedCharater}${indent}${" ".repeat(this.options.indentSpaceCount)}`;
        let items: string[] = [];
        if (cellStyle.width !== undefined) {
            items.push(`${itemIndentSpace}Width = "${cellStyle.width}"`);
        }
        if (cellStyle.height !== undefined) {
            items.push(`${itemIndentSpace}Height = "${cellStyle.height}"`);
        }
        if (cellStyle.borderColor !== undefined) {
            items.push(`${itemIndentSpace}BorderColor = "${cellStyle.borderColor}"`);
        }
        if (cellStyle.borderTopColor !== undefined) {
            items.push(`${itemIndentSpace}BorderTopColor = "${cellStyle.borderTopColor}"`);
        }
        if (cellStyle.borderRightColor !== undefined) {
            items.push(`${itemIndentSpace}BorderRightColor = "${cellStyle.borderRightColor}"`);
        }
        if (cellStyle.borderBottomColor !== undefined) {
            items.push(`${itemIndentSpace}BorderBottomColor = "${cellStyle.borderBottomColor}"`);
        }
        if (cellStyle.borderLeftColor !== undefined) {
            items.push(`${itemIndentSpace}BorderLeftColor = "${cellStyle.borderLeftColor}"`);
        }
        if (cellStyle.borderStyle !== undefined) {
            items.push(`${itemIndentSpace}BorderStyle = "${borderStyleToString(cellStyle.borderStyle)}"`);
        }
        if (cellStyle.borderWidth !== undefined) {
            items.push(`${itemIndentSpace}BorderWidth = ${cellStyle.borderWidth.toString()}`);
        }
        if (cellStyle.borderTopWidth !== undefined) {
            items.push(`${itemIndentSpace}BorderTopWidth = ${cellStyle.borderTopWidth.toString()}`);
        }
        if (cellStyle.borderRightWidth !== undefined) {
            items.push(`${itemIndentSpace}BorderRightWidth = ${cellStyle.borderRightWidth.toString()}`);
        }
        if (cellStyle.borderBottomWidth !== undefined) {
            items.push(`${itemIndentSpace}BorderBottomWidth = ${cellStyle.borderBottomWidth.toString()}`);
        }
        if (cellStyle.borderLeftWidth !== undefined) {
            items.push(`${itemIndentSpace}BorderLeftWidth = ${cellStyle.borderLeftWidth.toString()}`);
        }
        if (cellStyle.borderTopStyle !== undefined) {
            items.push(`${itemIndentSpace}BorderTopStyle = "${borderStyleToString(cellStyle.borderTopStyle)}"`);
        }
        if (cellStyle.borderRightStyle !== undefined) {
            items.push(`${itemIndentSpace}BorderRightStyle = "${borderStyleToString(cellStyle.borderRightStyle)}"`);
        }
        if (cellStyle.borderBottomStyle !== undefined) {
            items.push(`${itemIndentSpace}BorderBottomStyle = "${borderStyleToString(cellStyle.borderBottomStyle)}"`);
        }
        if (cellStyle.borderLeftStyle !== undefined) {
            items.push(`${itemIndentSpace}BorderLeftStyle = "${borderStyleToString(cellStyle.borderLeftStyle)}"`);
        }
        if (cellStyle.padding !== undefined) {
            items.push(`${itemIndentSpace}Padding = ${cellStyle.padding.toString()}`);
        }
        if (cellStyle.paddingTop !== undefined) {
            items.push(`${itemIndentSpace}PaddingTop = ${cellStyle.paddingTop.toString()}`);
        }
        if (cellStyle.paddingRight !== undefined) {
            items.push(`${itemIndentSpace}PaddingRight = ${cellStyle.paddingRight.toString()}`);
        }
        if (cellStyle.paddingBottom !== undefined) {
            items.push(`${itemIndentSpace}PaddingBottom = ${cellStyle.paddingBottom.toString()}`);
        }
        if (cellStyle.paddingLeft !== undefined) {
            items.push(`${itemIndentSpace}PaddingLeft = ${cellStyle.paddingLeft.toString()}`);
        }
        if (cellStyle.wrap !== undefined) {
            items.push(`${itemIndentSpace}Wrap = ${cellStyle.wrap ? "true" : "false"}`);
        }
        if (cellStyle.colSpan !== undefined) {
            items.push(`${itemIndentSpace}ColSpan = ${cellStyle.colSpan.toString()}`);
        }
        if (cellStyle.rowSpan !== undefined) {
            items.push(`${itemIndentSpace}RowSpan = ${cellStyle.rowSpan.toString()}`);
        }
        if (cellStyle.bgColor !== undefined) {
            items.push(`${itemIndentSpace}BgColor = "${cellStyle.bgColor}"`);
        }
        if (cellStyle.repeatHeader !== undefined) {
            items.push(`${itemIndentSpace}RepeatHeader = ${cellStyle.repeatHeader.toString()}`);
        }
        if (cellStyle.repeatSideHeader !== undefined) {
            items.push(`${itemIndentSpace}RepeatSideHeader = ${cellStyle.repeatSideHeader.toString()}`);
        }
        return `${indent}Cell(${items.join(",")}${this.options.lineFeedCharater}${indent})`;
    }

    private serializeCellStyleToXml(dom: XMLDocument, cellStyle: t.CellStyle): Element {
        const element = dom.createElement("property");
        const styles = dom.createElement("styles");
        if (cellStyle.borderColor !== undefined) {
            styles.appendChild(createXmlPropertyNode(dom, "BorderColor", cellStyle.borderColor, "8", cellStyle.context));
        }
        if (cellStyle.borderStyle !== undefined) {
            styles.appendChild(createXmlPropertyNode(dom, "BorderStyle", cellStyle.borderStyle.toString(), "3", cellStyle.context));
        }
        if (cellStyle.width !== undefined) {
            styles.appendChild(createXmlPropertyNode(dom, "CWidth", cellStyle.width, "8", cellStyle.context));
        }
        if (cellStyle.height !== undefined) {
            styles.appendChild(createXmlPropertyNode(dom, "CHeight", cellStyle.height, "8", cellStyle.context));
        }
        if (cellStyle.borderTopColor !== undefined) {
            styles.appendChild(createXmlPropertyNode(dom, "BorderTopColor", cellStyle.borderTopColor, "8", cellStyle.context));
        }
        if (cellStyle.borderRightColor !== undefined) {
            styles.appendChild(createXmlPropertyNode(dom, "BorderRightColor", cellStyle.borderRightColor, "8", cellStyle.context));
        }
        if (cellStyle.borderBottomColor !== undefined) {
            styles.appendChild(createXmlPropertyNode(dom, "BorderBottomColor", cellStyle.borderBottomColor, "8", cellStyle.context));
        }
        if (cellStyle.borderLeftColor !== undefined) {
            styles.appendChild(createXmlPropertyNode(dom, "BorderLeftColor", cellStyle.borderLeftColor, "8", cellStyle.context));
        }
        if (cellStyle.borderWidth !== undefined) {
            styles.appendChild(createXmlPropertyNode(dom, "BorderWidth", cellStyle.borderWidth.toString(), "3", cellStyle.context));
        }
        if (cellStyle.borderTopWidth !== undefined) {
            styles.appendChild(createXmlPropertyNode(dom, "BorderTopWidth", cellStyle.borderTopWidth.toString(), "3", cellStyle.context));
        }
        if (cellStyle.borderRightWidth !== undefined) {
            styles.appendChild(createXmlPropertyNode(dom, "BorderRightWidth", cellStyle.borderRightWidth.toString(), "3", cellStyle.context));
        }
        if (cellStyle.borderBottomWidth !== undefined) {
            styles.appendChild(createXmlPropertyNode(dom, "BorderBottomWidth", cellStyle.borderBottomWidth.toString(), "3", cellStyle.context));
        }
        if (cellStyle.borderLeftWidth !== undefined) {
            styles.appendChild(createXmlPropertyNode(dom, "BorderLeftWidth", cellStyle.borderLeftWidth.toString(), "3", cellStyle.context));
        }
        if (cellStyle.borderTopStyle !== undefined) {
            styles.appendChild(createXmlPropertyNode(dom, "BorderTopStyle", cellStyle.borderTopStyle.toString(), "3", cellStyle.context));
        }
        if (cellStyle.borderRightStyle !== undefined) {
            styles.appendChild(createXmlPropertyNode(dom, "BorderRightStyle", cellStyle.borderRightStyle.toString(), "3", cellStyle.context));
        }
        if (cellStyle.borderBottomStyle !== undefined) {
            styles.appendChild(createXmlPropertyNode(dom, "BorderBottomStyle", cellStyle.borderBottomStyle.toString(), "3", cellStyle.context));
        }
        if (cellStyle.borderLeftStyle !== undefined) {
            styles.appendChild(createXmlPropertyNode(dom, "BorderLeftStyle", cellStyle.borderLeftStyle.toString(), "3", cellStyle.context));
        }
        if (cellStyle.padding !== undefined) {
            styles.appendChild(createXmlPropertyNode(dom, "Padding", cellStyle.padding.toString(), "3", cellStyle.context));
        }
        if (cellStyle.paddingTop !== undefined) {
            styles.appendChild(createXmlPropertyNode(dom, "PaddingTop", cellStyle.paddingTop.toString(), "3", cellStyle.context));
        }
        if (cellStyle.paddingRight !== undefined) {
            styles.appendChild(createXmlPropertyNode(dom, "PaddingRight", cellStyle.paddingRight.toString(), "3", cellStyle.context));
        }
        if (cellStyle.paddingBottom !== undefined) {
            styles.appendChild(createXmlPropertyNode(dom, "PaddingBottom", cellStyle.paddingBottom.toString(), "3", cellStyle.context));
        }
        if (cellStyle.paddingLeft !== undefined) {
            styles.appendChild(createXmlPropertyNode(dom, "PaddingLeft", cellStyle.paddingLeft.toString(), "3", cellStyle.context));
        }
        if (cellStyle.wrap !== undefined) {
            styles.appendChild(createXmlPropertyNode(dom, "Wrap", booleanToString(cellStyle.wrap), "11", cellStyle.context));
        }
        if (cellStyle.colSpan !== undefined) {
            styles.appendChild(createXmlPropertyNode(dom, "ColSpan", cellStyle.colSpan.toString(), "3", cellStyle.context));
        }
        if (cellStyle.rowSpan !== undefined) {
            styles.appendChild(createXmlPropertyNode(dom, "RowSpan", cellStyle.rowSpan.toString(), "3", cellStyle.context));
        }
        if (cellStyle.bgColor !== undefined) {
            styles.appendChild(createXmlPropertyNode(dom, "CBgColor", cellStyle.bgColor, "8", cellStyle.context));
        }
        if (cellStyle.repeatHeader !== undefined) {
            styles.appendChild(createXmlPropertyNode(dom, "RepeatHeader", cellStyle.repeatHeader.toString(), "3", cellStyle.context));
        }
        if (cellStyle.repeatSideHeader !== undefined) {
            styles.appendChild(createXmlPropertyNode(dom, "RepeatSideHeader", cellStyle.repeatSideHeader.toString(), "3", cellStyle.context));
        }
        element.appendChild(styles);
        return element;
    }

    private serializeControlStyleToString(controlStyle: t.ControlStyle, indentLevel = 0): string {
        const indent = " ".repeat(indentLevel * this.options.indentSpaceCount);
        const itemIndentSpace = `${this.options.lineFeedCharater}${indent}${" ".repeat(this.options.indentSpaceCount)}`;
        let items: string[] = [];
        if (controlStyle.type !== undefined) {
            items.push(`${itemIndentSpace}Type = ${controlTypeToString(controlStyle.type)}`);
        }
        if (controlStyle.readonly !== undefined) {
            items.push(`${itemIndentSpace}ReadOnly = ${controlStyle.readonly ? "true" : "false"}`);
        }
        if (controlStyle.accelerator !== undefined) {
            items.push(`${itemIndentSpace}Accelerator = "${controlStyle.accelerator}"`);
        }
        return `${indent}Control(${items.join(",")}${this.options.lineFeedCharater}${indent})`;
    }

    private serializeControlStyleToXml(dom: XMLDocument, controlStyle: t.ControlStyle): Element {
        const element = dom.createElement("property");
        const styles = dom.createElement("styles");
        if (controlStyle.accelerator) {
            styles.appendChild(createXmlPropertyNode(dom, "Accelerator", controlStyle.accelerator, "8", controlStyle.context));
        }
        if (controlStyle.type !== undefined) {
            styles.appendChild(createXmlPropertyNode(dom, "Type", controlStyle.type.toString(), "3", controlStyle.context));
        }
        if (controlStyle.readonly !== undefined) {
            styles.appendChild(createXmlPropertyNode(dom, "ReadOnly", booleanToString(controlStyle.readonly), "11", controlStyle.context));
        }
        return element;
    }

    private serializeStyleToString(style: t.Style, isLabelStyles = false, indentLevel = 0): string {
        const indent = " ".repeat(indentLevel * this.options.indentSpaceCount);
        const elementBegin = `${this.options.lineFeedCharater}${indent}${" ".repeat(this.options.indentSpaceCount)}`;
        const items: string[] = [];
        if (style.color !== undefined) {
            items.push(`${elementBegin}Color = "${style.color}"`);
        }
        if (style.bgColor !== undefined) {
            items.push(`${elementBegin}BgColor = "${style.bgColor}"`);
        }
        if (style.hidden !== undefined) {
            items.push(`${elementBegin}Hidden = ${style.hidden ? "true" : "false"}`);
        }
        if (style.align !== undefined) {
            items.push(`${elementBegin}Align = "${alignmentToString(style.align)}"`);
        }
        if (style.verticalAlign !== undefined) {
            items.push(`${elementBegin}VerticalAlign = "${verticalAlignToString(style.verticalAlign)}"`);
        }
        if (style.elementAlign !== undefined) {
            items.push(`${elementBegin}ElementAlign = "${elementAlignToString(style.elementAlign)}"`);
        }
        if (style.indent !== undefined) {
            items.push(`${elementBegin}Indent = ${style.indent.toString()}`);
        }
        if (style.zIndex !== undefined) {
            items.push(`${elementBegin}ZIndex = ${style.zIndex.toString()}`);
        }
        if (style.cursor !== undefined) {
            items.push(`${elementBegin}Cursor = "${cursorTypeToString(style.cursor)}"`);
        }
        if (style.image !== undefined) {
            items.push(`${elementBegin}Image = "${style.image}"`);
        }
        if (style.imagePosition !== undefined) {
            items.push(`${elementBegin}ImagePosition = "${imagePositionToString(style.imagePosition)}"`);
        }
        if (style.orientation !== undefined) {
            items.push(`${elementBegin}Orientation = "${orientationToString(style.orientation)}"`);
        }
        if (style.columns !== undefined) {
            items.push(`${elementBegin}Columns = ${style.columns.toString()}`);
        }
        if (style.rows !== undefined) {
            items.push(`${elementBegin}Rows = ${style.rows.toString()}`);
        }
        if (style.width !== undefined) {
            items.push(`${elementBegin}Width = "${style.width}"`);
        }
        if (style.height !== undefined) {
            items.push(`${elementBegin}Height = "${style.height}"`);
        }
        if (style.control) {
            items.push(this.options.lineFeedCharater + this.serializeControlStyleToString(style.control, indentLevel + 1));
        }
        if (style.cell) {
            items.push(this.options.lineFeedCharater + this.serializeCellStyleToString(style.cell, indentLevel + 1));
        }
        if (style.font) {
            items.push(this.options.lineFeedCharater + this.serializeFontStyleToString(style.font, indentLevel + 1));
        }
        return `${this.options.lineFeedCharater}${indent}${isLabelStyles ? "labelstyle" : "style"}(${items.join(",")}${this.options.lineFeedCharater}${indent})`;
    }

    private serializeStyleToXml(dom: XMLDocument, style: t.Style): Element[] {
        const elements: Element[] = [];
        if (style.cell) {
            elements.push(this.serializeCellStyleToXml(dom, style.cell));
        }
        if (style.control) {
            elements.push(this.serializeControlStyleToXml(dom, style.control));
        }
        if (style.font) {
            elements.push(this.serializeFontStyleToXml(dom, style.font));
        }
        if (style.color !== undefined) {
            elements.push(createXmlPropertyNode(dom, "Color", style.color, "8", style.context));
        }
        if (style.bgColor !== undefined) {
            elements.push(createXmlPropertyNode(dom, "BgColor", style.bgColor, "8", style.context));
        }
        if (style.hidden !== undefined) {
            elements.push(createXmlPropertyNode(dom, "Hidden", booleanToString(style.hidden), "11", style.context));
        }
        if (style.align !== undefined) {
            elements.push(createXmlPropertyNode(dom, "Align", style.align.toString(), "3", style.context));
        }
        if (style.verticalAlign !== undefined) {
            elements.push(createXmlPropertyNode(dom, "VerticalAlign", style.verticalAlign.toString(), "3", style.context));
        }
        if (style.elementAlign !== undefined) {
            elements.push(createXmlPropertyNode(dom, "ElementAlign", style.elementAlign.toString(), "3", style.context));
        }
        if (style.indent !== undefined) {
            elements.push(createXmlPropertyNode(dom, "Indent", style.indent.toString(), "3", style.context));
        }
        if (style.zIndex !== undefined) {
            elements.push(createXmlPropertyNode(dom, "ZIndex", style.zIndex.toString(), "3", style.context));
        }
        if (style.cursor !== undefined) {
            elements.push(createXmlPropertyNode(dom, "Cursor", style.cursor.toString(), "3", style.context));
        }
        if (style.image !== undefined) {
            elements.push(createXmlPropertyNode(dom, "Image", style.image, "8", style.context));
        }
        if (style.imagePosition !== undefined) {
            elements.push(createXmlPropertyNode(dom, "ImagePosition", style.imagePosition.toString(), "3", style.context));
        }
        if (style.orientation !== undefined) {
            elements.push(createXmlPropertyNode(dom, "Orientation", style.orientation.toString(), "3", style.context));
        }
        if (style.columns !== undefined) {
            elements.push(createXmlPropertyNode(dom, "Columns", style.columns.toString(), "3", style.context));
        }
        if (style.rows !== undefined) {
            elements.push(createXmlPropertyNode(dom, "Rows", style.rows.toString(), "3", style.context));
        }
        if (style.width !== undefined) {
            elements.push(createXmlPropertyNode(dom, "Width", style.width, "8", style.context));
        }
        if (style.height !== undefined) {
            elements.push(createXmlPropertyNode(dom, "Height", style.height, "8", style.context));
        }
        return elements;
    }

    private serializeStylesToXml(dom: XMLDocument, styles: t.Styles, isLabelStyles = false): Element {
        const element = dom.createElement(isLabelStyles ? "labelstyles" : "styles");
        for (const style of styles) {
            this.serializeStyleToXml(dom, style.value).forEach(e => element.appendChild(e));
        }
        return element;
    }

    private serializeTemplateToString(template: t.Template, indentLevel = 0): string {
        const indent = " ".repeat(indentLevel * this.options.indentSpaceCount);
        return `${indent}${template.name}="${template.value}"`;
    }

    private serializeTemplatesToXml(dom: XMLDocument, templates: t.Templates): Element {
        const element = dom.createElement("templates");
        for (const contextItem of templates) {
            for (const temp of contextItem.value) {
                element.appendChild(createXmlPropertyNode(dom, temp.name, normalToHtmlEscapeCharater(temp.value), "8", temp.context));
            }
        }
        return element;
    }

    private serializeTemplatesToString(templates: t.Template[], indentLevel = 0): string {
        const indent = " ".repeat(indentLevel * this.options.indentSpaceCount);
        const items: string[] = [];
        for (const template of templates) {
            items.push(`${this.options.lineFeedCharater}${this.serializeTemplateToString(template, indentLevel + 1)}`);
        }
        return items.length === 0 ? "" : `${this.options.lineFeedCharater}${indent}templates(${items.join(",")}${this.options.lineFeedCharater}${indent})`;
    }

    private serializeMDMLabeledObjectToString(obj: t.IMDMLabeledObject, serializeName = false, indentLevel = 0, nameCallback?: () => string): string {
        const indent = " ".repeat(indentLevel * this.options.indentSpaceCount);
        const labels: string[] = [];
        const currentLabelType = obj.document.labelTypes.current;
        const contextLabel = obj.labels.getItem(currentLabelType);
        const lcl = obj.document.getLCLSet();
        const currentContext = obj.document.contexts.current;
        const contexts = [ currentContext ];
        if (lcl) {
            lcl.forEach(e => {
                if (!stringEqual(e, currentContext)) {
                    contexts.push(e);
                }
            });
        }
        contexts.forEach((v, i) => {
            let label = "-";
            if (contextLabel) {
                let lclLabel = contextLabel.getText(v, obj.document.languages.current);
                if (lclLabel !== undefined /** 描述字符内容允许空字符串 */) {
                    label = `"${lclLabel}"`;
                }
            }
            let item = `${i > 0 ? `${indent}${v.toUpperCase()}:` : ""}${label}`;
            let contextProperties = obj.properties.getItem(v);
            if (contextProperties && contextProperties.length > 0 && !this.options.hideLabeledObjectProperties) {
                item += this.serializePropertiesToString(contextProperties, indentLevel + 1);
            }
            let contextStyle = obj.styles?.getItem(v);
            if (contextStyle && !this.options.hideLabeledObjectProperties) {
                item += this.serializeStyleToString(contextStyle, false, indentLevel + 1);
            }
            let contextLabelStyle = obj.labelStyles?.getItem(v);
            if (contextLabelStyle && !this.options.hideLabeledObjectProperties) {
                item += this.serializeStyleToString(contextLabelStyle, true, indentLevel + 1);
            }
            let contextTemplates = obj.templates?.getItem(v);
            if (contextTemplates && !this.options.hideLabeledObjectProperties) {
                item += this.serializeTemplatesToString(contextTemplates, indentLevel + 1);
            }
            labels.push(item);
        });
        let note = "";
        if (obj.notes && obj.notes.length > 0) {
            note = this.serializeNotesToString(obj.notes) + this.options.lineFeedCharater;
        }
        return `${note}${serializeName ? `${indent}${nameCallback ? nameCallback() : obj.name} ` : ""}${labels.join(this.options.lineFeedCharater)}`;
    }

    private serializeLabelObjectToXml(dom: XMLDocument, labelObject: t.LabelObject): Element {
        const element = dom.createElement("text");
        element.setAttribute("context", labelObject.context);
        element.setAttribute("xml:lang", labelObject.language);
        const text = normalToHtmlEscapeCharater(labelObject.text);
        element.appendChild(labelObject.cdata ? dom.createCDATASection(text) : dom.createTextNode(text));
        return element;
    }

    private serializeLabelToXml(label: t.Label): Element {
        const dom = label.document.dom;
        const element = dom.createElement("labels");
        element.setAttribute("context", label.context);
        if (label.deleted.length > 0) {
            const deletedNode = dom.createElement("deleted");
            for (const del of label.deleted) {
                deletedNode.appendChild(this.serializeLabelObjectToXml(dom, del));
            }
            element.appendChild(deletedNode);
        }
        for (const labelObject of label) {
            element.appendChild(this.serializeLabelObjectToXml(dom, labelObject));
        }
        return element;
    }

    private serializeLabelsToXml(element: Element, labels: t.Labels): void {
        for (const contextLabel of labels) {
            element.appendChild(this.serializeLabelToXml(contextLabel.value));
        }
    }

    private serializeMDMLabeledObjectToXml(obj: t.IMDMLabeledObject, tagName: string, ignoreIdAndName: boolean = false, callback?: LabeledObjectCallback): Element {
        const dom = obj.document.dom;
        const element = dom.createElement(tagName);
        if (!ignoreIdAndName) {
            element.setAttribute("id", obj.uuid);
            element.setAttribute("name", obj.name);
        }
        // properties templates labelstyles styles
        this.serializePropertisStylesAndTemplatesToXml(dom, element, obj, callback);

        this.serializeLabelsToXml(element, obj.labels);
        if (obj.notes) {
            element.appendChild(this.serializeNotesToXml(dom, obj.notes));
        }
        return element;
    }

    private serializeDisplayOrderConstantsToXml(obj: { readonly displayOrder: t.DisplayOrders, readonly document: t.BaseMDMDocument }): Element | undefined {
        if (obj.displayOrder > t.DisplayOrders.normal) {
            return createXmlPropertyNode(obj.document.dom, "DisplayOrder", displayOrderToXmlValue(obj.displayOrder), "8", "Question");
        }
    }

    private serializeDisplayOrientationToXml(obj: { readonly orientation?: t.DisplayOrientations } & t.IMDMBase): Element | undefined {
        if (obj.orientation === t.DisplayOrientations.col) {
            return createXmlPropertyNode(obj.document.dom, "DisplayOrientation", "Vertical", "8", "Question");
        }
        else if (obj.orientation === t.DisplayOrientations.row) {
            return createXmlPropertyNode(obj.document.dom, "DisplayOrientation", "Horizontal", "8", "Question");
        }
    }

    private serializeMaybeStringValueToString(value: string | number | boolean): string {
        return typeof value === "string" ? `"${value}"` : value.toString();
    }

    private serializeRangeToString(range: t.Range): string {
        return `${range.lowerBound === this.options.invalidValueLowerBoundary ? "" : this.serializeMaybeStringValueToString(range.lowerBound)}..${range.upperBound === this.options.invalidValueUpperBoundary ? "" : this.serializeMaybeStringValueToString(range.upperBound)}`;
    }

    private serializeRangeToXml(dom: XMLDocument, range: t.Range): Element {
        const element = dom.createElement("range");
        element.setAttribute("upperbound", range.upperBound.toString());
        element.setAttribute("lowerbound", range.lowerBound.toString());
        return element;
    }

    private serializeRangesToString(ranges: t.Ranges): string {
        return `[${ranges.map(e => this.serializeRangeToString(e)).join(", ")}]`;
    }

    private serializeRangesToXml(dom: XMLDocument, ranges: t.Ranges): Element {
        const element = dom.createElement("ranges");
        for (const range of ranges) {
            element.appendChild(this.serializeRangeToXml(dom, range));
        }
        return element;
    }

    private serializeMDMListToXml<T>(dom: XMLDocument, list: t.MDMListOf<T>, specifiedTag: string, serializer: (obj: T) => Element | undefined, preCallback?: (resultNode: Element) => void): Element {
        const element = dom.createElement(specifiedTag);
        if (preCallback) {
            preCallback(element);
        }
        const deleted = dom.createElement("deleted");
        for (const deletedItem of list.deleted) {
            let deletedNode = serializer(deletedItem);
            if (deletedNode) {
                deleted.appendChild(deletedNode);
            }
        }
        element.appendChild(deleted);
        for (const item of list) {
            let itemNode = serializer(item);
            if (itemNode) {
                element.appendChild(itemNode);
            }
        }
        return element;
    }

    private serializeTypesToXml<T extends t.IMDMObject>(dom: XMLDocument, types: t.Types<T>, specifiedTag: string, serializer: (obj: T) => Element | undefined): Element {
        const element = dom.createElement(specifiedTag);
        const deletedNode = dom.createElement("deleted");
        types.deleted?.forEach(type => {
            let node = serializer(type);
            if (node) {
                deletedNode.appendChild(node);
            }
        });
        element.appendChild(deletedNode);
        types.forEach(type => {
            let node = serializer(type);
            if (node) {
                element.appendChild(node);
            }
        });
        // unversioned
        if (types.unversioned) {
            const unversionedNode = dom.createElement("unversioned");
            types.unversioned.forEach(e => {
                let node = serializer(e);
                if (node) {
                    unversionedNode.appendChild(node);
                }
            });
            element.appendChild(unversionedNode);
        }
        return element;
    }

    private serializeNoteToString(note: t.Note, indentLevel = 0): string {
        return `${" ".repeat(indentLevel * this.options.indentSpaceCount)}'${note.value}`;
    }

    private serializeNoteToXml(dom: Document, note: t.Note): Element {
        return createXmlPropertyNode(dom, note.name, normalToHtmlEscapeCharater(note.value), "8", note.context);
    }

    private serializeNotesToString(notes: t.Notes, indentLevel = 0): string {
        let result: string[] = [];
        for (const note of notes) {
            result.push(this.serializeNoteToString(note, indentLevel));
        }
        return result.join(this.options.lineFeedCharater);
    }

    private serializeNotesToXml(dom: Document, notes: t.Notes): Element {
        const element = dom.createElement("notes");
        for (const note of notes) {
            element.appendChild(this.serializeNoteToXml(dom, note));
        }
        return element;
    }

    private serializeScriptToXml(dom: Document, script: t.Script): Element {
        const element = dom.createElement("script");
        element.setAttribute("name", script.name);
        element.setAttribute("default", booleanToString(script.isDefault));
        element.appendChild(dom.createTextNode(normalToHtmlEscapeCharater(script.script)));
        return element;
    }

    private serializeScriptTypeToXml(dom: Document, scriptType: t.ScriptType): Element{
        const element = this.serializeMDMListToXml(dom, scriptType, "scripttype", e => this.serializeScriptToXml(dom, e));
        element.setAttribute("type", scriptType.type);
        element.setAttribute("context", scriptType.context);
        element.setAttribute("interviewmodes", scriptType.interviewMode.toString());
        element.setAttribute("usekeycodes", booleanToString(scriptType.useKeyCode));
        return element;
    }

    private serializeScriptsToXml(dom: Document, scripts: t.Scripts): Element {
        return this.serializeMDMListToXml(dom, scripts, "scripts", e => this.serializeScriptTypeToXml(dom, e));
    }

    private serializeRoutingItemToXml(dom: Document, routingItem: t.RoutingItem): Element {
        const element = dom.createElement("ritem");
        element.setAttribute("name", routingItem.name);
        element.setAttribute("item", routingItem.item);
        return element;
    }

    private serializeRoutingToXml(dom: Document, routing: t.Routing): Element {
        const element = this.serializeMDMListToXml(dom, routing, "routing", e => this.serializeRoutingItemToXml(dom, e));
        element.setAttribute("context", routing.context);
        element.setAttribute("interviewmodes", routing.interviewMode.toString());
        element.setAttribute("usekeycodes", booleanToString(routing.useKeyCode));
        return element;
    }

    private serializeRoutingsToXml(dom: Document, routings: t.Routings, specifiedTag?: string): Element {
        const element = this.serializeMDMListToXml(dom, routings, specifiedTag ?? "routings",
            e => this.serializeRoutingToXml(dom, e),
            e => {
                e.appendChild(this.serializeScriptsToXml(dom, routings.scripts));
                if (routings.properties.length > 0) {
                    e.appendChild(this.serializePropertiesToXml(dom, routings.properties));
                }
            });
        if (routings.name) {
            element.setAttribute("name", routings.name);
        }
        return element;
    }

    private serializePropertisStylesAndTemplatesToXml(
        dom: Document,
        element: Element,
        obj: {
            readonly properties: t.Properties,
            styles?: t.Styles,
            labelstyles?: t.Styles,
            templates?: t.Templates
        },
        onElementFinished?: LabeledObjectCallback) {
        // properties
        let propElement: Element | undefined;
        if (obj.properties.length > 0) {
            propElement = this.serializePropertiesToXml(dom, obj.properties);
            element.appendChild(propElement);
        }
        if (onElementFinished && onElementFinished.properties) {
            onElementFinished.properties.bind(this)(propElement, element);
        }
        // styles
        let stylesElement: Element | undefined;
        if (obj.styles) {
            stylesElement = this.serializeStylesToXml(dom, obj.styles);
            element.appendChild(stylesElement);
        }
        if (onElementFinished && onElementFinished.styles) {
            onElementFinished.styles.bind(this)(stylesElement, element);
        }
        // labelStyles
        let labelStylesElement: Element | undefined;
        if (obj.labelstyles) {
            labelStylesElement = this.serializeStylesToXml(dom, obj.labelstyles, true);
            element.appendChild(labelStylesElement);
        }
        if (onElementFinished && onElementFinished.labelStyles) {
            onElementFinished.labelStyles.bind(this)(labelStylesElement, element);
        }
        // templates
        let templatesElement: Element | undefined;
        if (obj.templates) {
            templatesElement = this.serializeTemplatesToXml(dom, obj.templates);
            element.appendChild(templatesElement);
        }
        if (onElementFinished && onElementFinished.templates) {
            onElementFinished.templates.bind(this)(templatesElement, element);
        }
    }

    // <category> ::= category_name
    //    [ "category_label" ]
    //    [ <other> | <multiplier> | DK | REF | NA ]
    //    [ exclusive ]
    //    [ factor (factor_value) ]
    //    [ keycode ("keycode_value") ]
    //    [ expression ("exp_text") ]
    //    [ elementtype (type_value) ]
    //    [ fix ]
    //    [ nofilter ]
    private serializeElementToString(element: t.MDMElement, indentLevel = 0): string {
        const indent = " ".repeat(indentLevel * this.options.indentSpaceCount);
        let result = this.serializeMDMLabeledObjectToString(element, true, indentLevel, () => {
            //if (stringEqual(element.name, [ "REF", "DK", "NA", "Other", "Multiplier" ])) {
            if ((stringEqual(element.name, "REF") && bitAnd(element.flag, t.CategoryFlags.refuse)) ||
                (stringEqual(element.name, "DK") && bitAnd(element.flag, t.CategoryFlags.dontknow)) ||
                (stringEqual(element.name, "NA") && bitAnd(element.flag, t.CategoryFlags.noanswer)) ||
                (stringEqual(element.name, "Other") && bitAnd(element.flag, t.CategoryFlags.other))) {
                return "-";
            }
            // reserved word
            let reservedWord = reservedName.get(element.name.toLowerCase());
            if (reservedWord) {
                return `"${reservedWord}"`;
            }
            return element.name;
        });
        const suffixes: string[] = [];
        if (bitAnd(element.flag, t.CategoryFlags.other)) {
            suffixes.push(`other${element.otherVariable ? `(${this.options.lineFeedCharater}${this.serializeVariableToString(element.otherVariable, indentLevel + 1)}${this.options.lineFeedCharater}${indent})` : ""}`);
        }
        if (bitAnd(element.flag, t.CategoryFlags.multiplier) && element.multiplierVariable) {
            suffixes.push(`multiplier(${this.options.lineFeedCharater}${this.serializeVariableToString(element.multiplierVariable, indentLevel + 1)}${this.options.lineFeedCharater}${indent})`);
        }
        if (bitAnd(element.flag, t.CategoryFlags.dontknow)) {
            suffixes.push("DK");
        }
        if (bitAnd(element.flag, t.CategoryFlags.refuse)) {
            suffixes.push("REF");
        }
        if (bitAnd(element.flag, t.CategoryFlags.noanswer)) {
            suffixes.push("NA");
        }
        if (bitAnd(element.flag, t.CategoryFlags.fixedPosition) && !bitAnd(element.flag, t.CategoryFlags.missing)) {
            suffixes.push("fix");
        }
        if (bitAnd(element.flag, t.CategoryFlags.exclusive) && !bitAnd(element.flag, t.CategoryFlags.missing)) {
            suffixes.push("exclusive");
        }
        if (bitAnd(element.flag, t.CategoryFlags.noFilter) && !bitAnd(element.flag, t.CategoryFlags.missing)) {
            suffixes.push("nofilter");
        }
        if (element.keyCode) {
            suffixes.push(`keycode("${element.keyCode}")`);
        }
        if (isScriptElementType(element.type)) {
            suffixes.push(`elementtype(${elementTypeConstantsToTypeName(element.type)})`);
        }
        if (element.factor !== undefined) {
            suffixes.push(`factor(${typeof element.factor === "string" ? `"${element.factor}"` : typeof element.factor === "number" ? element.factor.toString() : element.factor ? "true" : "false"})`);
        }
        if (element.expression !== undefined) {
            suffixes.push(`expression("${element.expression}"${element.autoElements ? ", DeriveElements" : ""})`);
        }
        if (suffixes.length > 0) {
            result += this.options.lineFeedCharater + indent + suffixes.join(" ");
        }
        return result;
    }


    private serializeElementSuffixVariable(tagName: string, variable: t.Variable | undefined, ele: t.MDMElement, element: Element) {
        if (!variable) {
            return;
        }
        let node = ele.document.dom.createElement(tagName);
        node.setAttribute("id", variable.uuid);
        node.setAttribute("name", variable.name);
        if (variable.reference) {
            node.setAttribute("ref", variable.reference.uuid);
        }
        else {
            this.error(errorMessages.variableNeedReference, [ variable ], variable.uuid);
        }
        element.appendChild(node);
    }

    private serializeElementToXml(ele: t.MDMElement): Element {
        const element = this.serializeMDMLabeledObjectToXml(ele, ele.type === t.ElementTypes.category ? "category" : "element");
        if (ele.type !== t.ElementTypes.category) {
            element.setAttribute("type", ele.type.toString());
        }
        if (ele.keyCode) {
            element.setAttribute("keycode", ele.keyCode);
        }
        if (ele.factor !== undefined) {
            element.setAttribute("factor-value", ele.factor.toString());
            element.setAttribute("factor-type", getFactorType(ele.factor));
        }
        if (bitAnd(ele.flag, t.CategoryFlags.fixedPosition)) {
            element.setAttribute("fixed", "-1");
        }
        if (bitAnd(ele.flag, t.CategoryFlags.noFilter)) {
            element.setAttribute("nofilter", "-1");
        }
        // DK/NA/REF
        if (bitAnd(ele.flag, t.CategoryFlags.dontknow)) {
            element.setAttribute("missing", "dontknow");
        }
        else if (bitAnd(ele.flag, t.CategoryFlags.noanswer)) {
            element.setAttribute("missing", "noanswer");
        }
        else if (bitAnd(ele.flag, t.CategoryFlags.refuse)) {
            element.setAttribute("missing", "refuse");
        }
        if (ele.expression !== undefined) {
            element.setAttribute("expression", ele.expression);
        }
        if (bitAnd(ele.flag, t.CategoryFlags.exclusive)) {
            element.setAttribute("exclusive", "-1");
        }
        if (bitAnd(ele.flag, t.CategoryFlags.other)) {
            element.setAttribute("other-local", "-1");
        }
        if (bitAnd(ele.flag, t.CategoryFlags.multiplier)) {
            element.setAttribute("multiplier-local", "-1");
        }

        // multiplier-variable
        this.serializeElementSuffixVariable("multiplier-variable", ele.multiplierVariable, ele, element);
        // othervariable
        this.serializeElementSuffixVariable("othervariable", ele.otherVariable, ele, element);
        return element;
    }

    private serializeElementsToString(elements: t.MDMElements, seiralizeNameLabel = true, indentLevel = 0, subField = false): string {
        const indent = " ".repeat(indentLevel * this.options.indentSpaceCount);
        let result = indent;
        if (elements.reference && elements.ownerField) {
            // <category> ::= [ list_name ]
            //                use define_list
            //                [ sublist [ rot[ate] | ran[domize] | rev[erse] |
            //                    asc[ending] | desc[ending] ] ]
            //                [ "list_label" ]
            //                [ fix ]
            if (elements.name !== "") {
                result += elements.name + " ";
            }
            let refName = elements.referenceName ?? elements.reference.name;
            if (subField) {
                result += `${refName} use \\\\.${refName}`;
            }
            else {
                result += `use ${refName}`;
            }
            if (elements.inline) {
                result += " sublist";
                if (elements.labels.length > 0) {
                    result += ` ${this.serializeMDMLabeledObjectToString(elements, false, indentLevel + 1)}`;
                }
            }
        }
        else {
            if (elements.reference) {
                elements = elements.reference;
            }
            // <categories> ::= { <category> (, <category>)* }
            //      [ rot[ate] | ran[domize] | rev[erse] |
            //          asc[ending] | desc[ending] ]
            //      [ fix ]
            //      [ namespace ]
            const categories: string[] = [];
            for (const ele of elements) {
                if (isMDMElement(ele)) {
                    categories.push(`${this.options.lineFeedCharater}${this.serializeElementToString(ele, indentLevel + 1)}`);
                }
                else if (isMDMElements(ele)) {
                    categories.push(`${this.options.lineFeedCharater}${this.serializeElementsToString(ele, false, indentLevel + 1)}`);
                }
            }
            result = `${elements.name !== "" && seiralizeNameLabel ? indent + elements.name + " " : ""}${seiralizeNameLabel ? this.serializeMDMLabeledObjectToString(elements, false, indentLevel) : ""}${isMDMDocument(elements.parent) ? `${this.options.lineFeedCharater}${indent}define` : ""}${this.options.lineFeedCharater}${indent}{${categories.join(",")}${this.options.lineFeedCharater}${indent}}`;
        }
        let suffixes: string[] = [];
        if (elements.fixed) {
            suffixes.push("fix");
        }
        if (elements.displayOrder > t.DisplayOrders.normal) {
            suffixes.push(displayOrderToScript(elements.displayOrder));
        }
        if (elements.namespace && !isMDMDocument(elements.parent)) {
            suffixes.push("namespace");
        }
        if (suffixes.length > 0) {
            result += `${elements.reference ? `${this.options.lineFeedCharater}${indent}` : " "}${suffixes.join(" ")}`;
        }
        return result;
    }

    private serailizeElementItem(targetElement: Element, sourceArray: readonly t.IMDMLabeledObject[]) {
        for (const ele of sourceArray) {
            if (isMDMElement(ele)) {
                targetElement.appendChild(this.serializeElementToXml(ele));
            }
            else if (isMDMElements(ele)) {
                targetElement.appendChild(this.serializeElementsToXml(ele));
            }
        }
    }

    private serializeElementsToXml(elements: t.MDMElements): Element {
        const dom = elements.document.dom;
        const parentIsElements = isMDMElements(elements.parent);
        const element = this.serializeMDMLabeledObjectToXml(elements, "categories", isMDMVariable(elements.parent), {
            properties: (e, r) => {
                const order = this.serializeDisplayOrderConstantsToXml(elements);
                if (order) {
                    let prop = e;
                    if (!prop) {
                        prop = elements.document.dom.createElement("properties");
                        if (r) {
                            r.appendChild(prop);
                        }
                    }
                    prop.appendChild(order);
                }
            }
        });
        if (elements.reference) {
            if (!parentIsElements) {
                element.setAttribute("ref", elements.reference.uuid);
                return element;
            }
            element.setAttribute("categoriesref", elements.reference.uuid);
        }
        if (!elements.namespace) {
            element.setAttribute("global-name-space", "-1");
        }
        if (elements.fixed) {
            element.setAttribute("fixed", "-1");
        }
        if (elements.referenceName) {
            element.setAttribute("ref_name", elements.referenceName);
        }

        const deletedNode = dom.createElement("deleted");
        this.serailizeElementItem(deletedNode, elements.deleted);
        element.appendChild(deletedNode);
        this.serailizeElementItem(element, elements);

        return element;
    }

    private serializeClassToString(classObject: t.Class, indentLevel = 0): string {
        const indent = " ".repeat(indentLevel * this.options.indentSpaceCount);
        const items: string[] = [];
        // block
        if (Array.isArray(classObject.parent)) {
            classObject.types.forEach(e => items.push(this.serializeElementsToString(e, true, indentLevel + 1) + ";"));
            classObject.fields.forEach(e => items.push(this.serializeFieldLikeToString(e, indentLevel + 1)));
            return `${indent}${this.serializeMDMLabeledObjectToString(classObject, true, indentLevel)} block fields (${this.options.lineFeedCharater}${items.join(this.options.lineFeedCharater)}${this.options.lineFeedCharater}${indent})`;
        }
        return ``;
    }

    private serializeClassToXml(classObject: t.Class): Element {
        const dom = classObject.document.dom;
        const element = this.serializeMDMLabeledObjectToXml(classObject, "class", true);
        if (!isMDMField(classObject.parent)) {
            element.setAttribute("id", classObject.uuid);
        }
        element.setAttribute("name", classObject.name);
        element.setAttribute("global-name-space", classObject.namespace ? "0" : "-1");
        // types
        const types = this.serializeTypesToXml(dom, classObject.types, "types", this.serializeElementsToXml);
        types.setAttribute("name", "@types");
        types.setAttribute("global-name-space", "-1");
        element.appendChild(types);
        // fields
        const fields = this.serializeTypesToXml(dom, classObject.fields, "fields", this.serializeFieldLikeToXml);
        fields.setAttribute("name", "@fields");
        fields.setAttribute("global-name-space", "-1");
        element.appendChild(fields);
        // pages
        if (classObject.pages) {
            const pages = this.serializeTypesToXml(dom, classObject.pages, "pages", this.serializePageToXml);
            pages.setAttribute("name", "@pages");
            pages.setAttribute("global-name-space", "-1");
            element.appendChild(pages);
        }
        // routings
        if (classObject.routings) {
            element.appendChild(this.serializeRoutingsToXml(dom, classObject.routings));
        }
        return element;
    }

    private serializeDBQuestionPropertiesToString(properties: t.DBQuestionProperties, indentLevel = 0): string {
        if (!properties) {
            return "";
        }
        const indent = " ".repeat(indentLevel * this.options.indentSpaceCount);
        const lv1Indent = `${indent}${" ".repeat(this.options.indentSpaceCount)}`;
        const lv2Indent = `${lv1Indent}${" ".repeat(this.options.indentSpaceCount)}`;
        const elementPrefix = this.options.lineFeedCharater + lv1Indent;
        let elements: string[] = [];
        elements.push(`${elementPrefix}ConnectionString = "${properties.connectionString}"`);
        elements.push(`${elementPrefix}Table = "${properties.table}"`);
        if (properties.minAnswers !== undefined) {
            elements.push(`${elementPrefix}MinAnswers = ${properties.minAnswers.toString()}`);
        }
        if (properties.maxAnswers !== undefined) {
            elements.push(`${elementPrefix}MaxAnswers = ${properties.maxAnswers.toString()}`);
        }
        if (properties.sqlFilter) {
            elements.push(`${elementPrefix}SQLFilter = "${properties.sqlFilter}"`);
        }
        if (properties.cacheTimeout !== undefined) {
            elements.push(`${elementPrefix}CacheTimeout = ${properties.cacheTimeout.toString()}`);
        }
        if (properties.iteratorIDType !== undefined) {
            elements.push(`${elementPrefix}IteratorIDType = "${properties.iteratorIDType}"`);
        }
        if (properties.columns) {
            let columnElements: string[] = [];
            const columnElementPrefix = this.options.lineFeedCharater + lv2Indent;
            columnElements.push(`${columnElementPrefix}ID = "${properties.columns.id}"`);
            columnElements.push(`${columnElementPrefix}Label = "${properties.columns.label}"`);
            if (properties.columns.keyCode) {
                columnElements.push(`${columnElementPrefix}KeyCode = "${properties.columns.keyCode}"`);
            }
            if (properties.columns.file) {
                columnElements.push(`${columnElementPrefix}File = "${properties.columns.file}"`);
            }
            if (properties.columns.analysisValue) {
                columnElements.push(`${columnElementPrefix}AnalysisValue = "${properties.columns.analysisValue}"`);
            }
            if (properties.columns.fixed) {
                columnElements.push(`${columnElementPrefix}Fixed = "${properties.columns.fixed}"`);
            }
            if (properties.columns.exclusive) {
                columnElements.push(`${columnElementPrefix}Exclusive = "${properties.columns.exclusive}"`);
            }
            elements.push(`${elementPrefix}Columns${this.options.lineFeedCharater}${lv1Indent}(${columnElements.join(",")}${this.options.lineFeedCharater}${lv1Indent})`);
        }
        return elements.length > 0 ?`${this.options.lineFeedCharater}${indent}db${this.options.lineFeedCharater}${indent}(${elements.join(",")}${this.options.lineFeedCharater}${indent})` : "";
    }

    private serializeDBQuestionPropertiesToXml(dom: Document, dbQuestionProperties: t.DBQuestionProperties): Element {
        const element = dom.createElement("db");
        // db 属性默认是 QUESTION 上下文类型
        if (dbQuestionProperties.columns?.id) {
            element.appendChild(createXmlPropertyNode(dom, "id", dbQuestionProperties.columns.id, "8", "Question"));
        }
        if (dbQuestionProperties.columns?.label) {
            element.appendChild(createXmlPropertyNode(dom, "Label", dbQuestionProperties.columns.label, "8", "Question"));
        }
        if (dbQuestionProperties.cacheTimeout !== undefined) {
            element.appendChild(createXmlPropertyNode(dom, "CacheTimeout", dbQuestionProperties.cacheTimeout.toString(), "3", "Question"));
        }
        if (dbQuestionProperties.maxAnswers !== undefined) {
            element.appendChild(createXmlPropertyNode(dom, "MaxAnswers", dbQuestionProperties.maxAnswers.toString(), "3", "Question"));
        }
        if (dbQuestionProperties.minAnswers !== undefined) {
            element.appendChild(createXmlPropertyNode(dom, "MinAnswers", dbQuestionProperties.minAnswers.toString(), "3", "Question"));
        }
        element.appendChild(createXmlPropertyNode(dom, "Table", dbQuestionProperties.table, "8", "Question"));
        element.appendChild(createXmlPropertyNode(dom, "ConnectionString", dbQuestionProperties.connectionString, "8", "Question"));
        if (dbQuestionProperties.iteratorIDType !== undefined) {
            element.appendChild(createXmlPropertyNode(dom, "IteratorIDType", dbQuestionProperties.iteratorIDType.toString(), "3", "Question"));
        }
        if (dbQuestionProperties.sqlFilter) {
            element.appendChild(createXmlPropertyNode(dom, "SQLFilter", dbQuestionProperties.sqlFilter, "8", "Question"));
        }
        if (dbQuestionProperties.columns?.keyCode) {
            element.appendChild(createXmlPropertyNode(dom, "KeyCode", dbQuestionProperties.columns.keyCode, "8", "Question"));
        }
        if (dbQuestionProperties.columns?.file) {
            element.appendChild(createXmlPropertyNode(dom, "File", dbQuestionProperties.columns.file, "8", "Question"));
        }
        if (dbQuestionProperties.columns?.analysisValue) {
            element.appendChild(createXmlPropertyNode(dom, "AnalysisValue", dbQuestionProperties.columns.analysisValue, "8", "Question"));
        }
        if (dbQuestionProperties.columns?.fixed) {
            element.appendChild(createXmlPropertyNode(dom, "Fixed", dbQuestionProperties.columns.fixed, "8", "Question"));
        }
        if (dbQuestionProperties.columns?.exclusive) {
            element.appendChild(createXmlPropertyNode(dom, "Exclusive", dbQuestionProperties.columns.exclusive, "8", "Question"));
        }
        if (dbQuestionProperties.cacheConnection !== undefined) {
            element.appendChild(createXmlPropertyNode(dom, "CacheConnection", booleanToString(dbQuestionProperties.cacheConnection), "11", "Question"));
        }
        return element;
    }

    private serializeFieldSuffixToString(field: t.Field, indentLevel = 0): string {
        const indent = " ".repeat(indentLevel * this.options.indentSpaceCount);
        let normalSuffix: string[] = [];
        // helperfields
        if (field.helperFields.size > 0) {
            normalSuffix.push(`${this.options.lineFeedCharater}${this.serializeHelperFieldsToString(field.helperFields, indentLevel)}`);
        }
        // nocasedata
        if (!field.hasCaseData) {
            normalSuffix.push("nocasedata");
        }
        // expression("")
        if (field.expression) {
            normalSuffix.push(`expression("${field.expression}"${field.autoElements ? ", DeriveElements" : ""})`);
        }
        // usagetype()
        if (isMDMVariable(field) && field.usageType !== t.VariableUsageFlags.variable) {
            normalSuffix.push(`usagetype("${variableUsageFlagsToString(field.usageType)}")`);
        }
        // axis("")
        if (field.axisExpression) {
            normalSuffix.push(`axis("${field.axisExpression}")`);
        }
        // defaultanswer
        if (field.defaultAnswer !== undefined) {
            normalSuffix.push(`defaultanswer(${formateValueByDataType(field.defaultAnswer.toString(), field.dataType)})`);
        }
        // initialanswer
        if (field.initialAnswer !== undefined) {
            normalSuffix.push(`initialanswer(${formateValueByDataType(field.initialAnswer.toString(), field.dataType)})`);
        }
        // validation
        if (field.validation) {
            normalSuffix.push(`validation("${field.validation}")`);
        }
        // precision
        if (field.precisions !== undefined) {
            normalSuffix.push(`precision(${field.precisions.toString()})`);
        }
        // scale
        if (field.scale !== undefined) {
            normalSuffix.push(`scale(${field.scale.toString()})`);
        }
        // codes
        if (field.codes) {
            normalSuffix.push(`codes(${this.serializeElementsToString((field.codes.reference ?? field.codes).elements, false, indentLevel + 1)}${this.options.lineFeedCharater}${indent})`);
        }
        // unversioned
        if (field.versioned === false) {
            normalSuffix.push("unversioned");
        }
        return normalSuffix.length > 0 ? " " + normalSuffix.join(" ") : "";
    }

    private serializeVariableToString(variable: t.Variable, indentLevel = 0, original?: t.MDMField): string {
        const indent = " ".repeat(indentLevel * this.options.indentSpaceCount);
        let result = this.serializeMDMLabeledObjectToString(variable, true, indentLevel);
        if (variable.dataType === t.DataTypes.none) {
            return result + this.options.lineFeedCharater + indent + "info";
        }
        result += this.options.lineFeedCharater + indent + dataTypeToString(variable.dbDataType !== undefined ? variable.dbDataType : variable.dataType);
        if (variable.rangeExpression) {
            result += variable.rangeExpression;
        }
        else if (variable.ranges) {
            result += this.serializeRangesToString(variable.ranges);
        }
        else if (variable.minValue !== undefined || variable.maxValue !== undefined) {
            result += `[${typeof variable.minValue === "string" ? `"${variable.minValue}"` : typeof variable.minValue === "number" ? variable.minValue.toString() : ""}..${typeof variable.maxValue === "string" ? `"${variable.maxValue}"` : typeof variable.maxValue === "number" ? variable.maxValue.toString() : ""}]`;
        }
        // db
        if (variable.dbQuestionProperties) {
            result += this.serializeDBQuestionPropertiesToString(variable.dbQuestionProperties, indentLevel);
        }
        if (variable.elements.length > 0) {
            result += this.serializeElementsToString(variable.elements, false, indentLevel, isMDMClass(original?.parent));
        }
        return result + this.serializeFieldSuffixToString(variable, indentLevel);
    }

    private serializeVariableToXml(variable: t.Variable, specifyTag?: string): Element {
        let tag = specifyTag ? specifyTag
                : variable.usageType === t.VariableUsageFlags.multiplier ? "multiplier-variable"
                : variable.usageType === t.VariableUsageFlags.otherSpecify ? "othervariable" : "variable";

        const element = this.serializeMDMLabeledObjectToXml(variable, tag, false);

        if (variable.reference) {
            element.setAttribute("ref", variable.uuid);
            return element;
        }

        element.setAttribute("type", variable.dataType.toString());

        if (variable.infoType !== undefined) {
            element.setAttribute("infotype", infoTypeToString(variable.infoType));
        }
        if (!variable.versioned) {
            element.setAttribute("unversioned", "-1");
        }
        if (variable.minValue !== undefined) {
            element.setAttribute("min", variable.minValue.toString());
            element.setAttribute("mintype", mdmValueToXmlType(variable.minValue, variable.dataType));
        }
        if (variable.maxValue !== undefined) {
            element.setAttribute("max", variable.maxValue.toString());
            element.setAttribute("maxtype", mdmValueToXmlType(variable.maxValue, variable.dataType));
        }
        if (variable.rangeExpression) {
            element.setAttribute("rangeexp", variable.rangeExpression);
        }
        if (variable.defaultAnswer !== undefined) {
            element.setAttribute("defaultanswer", variable.defaultAnswer.toString());
            element.setAttribute("defaultanswer_type", mdmValueToXmlType(variable.defaultAnswer, variable.dataType));
        }
        if (variable.initialAnswer !== undefined) {
            element.setAttribute("initialanswer", variable.initialAnswer.toString());
            element.setAttribute("initialanswer_type", mdmValueToXmlType(variable.initialAnswer, variable.dataType));
        }
        if (variable.usageType !== t.VariableUsageFlags.variable) {
            element.setAttribute("usagetype", variable.usageType.toString());
        }
        if (!variable.hasCaseData) {
            element.setAttribute("no-casedata", "-1");
        }
        if (variable.precisions !== undefined) {
            element.setAttribute("precisions", variable.precisions.toString());
        }
        if (variable.scale !== undefined) {
            element.setAttribute("scale", variable.scale.toString());
        }
        if (variable.expression !== undefined) {
            element.setAttribute("expression", variable.expression);
        }
        if (variable.sourceType !== t.SourceTypes.none) {
            element.setAttribute("sourcetype", variable.sourceType.toString());
        }
        if (variable.validation !== undefined) {
            element.setAttribute("validation", variable.validation);
        }
        // db
        if (variable.dbQuestionProperties) {
            element.appendChild(this.serializeDBQuestionPropertiesToXml(variable.document.dom, variable.dbQuestionProperties));
        }
        element.appendChild(this.serializeElementsToXml(variable.elements));
        let helperFields = this.serializeHelperFieldsToXml(variable.document.dom, variable.helperFields);
        if (variable.codes) {
            helperFields.appendChild(this.serializeVariableToXml(variable.codes));
        }
        element.appendChild(helperFields);

        return element;
    }

    private serializeHelperFieldsToString(fields: t.Types<t.Variable>, indentLevel = 0): string {
        const indent = " ".repeat(indentLevel * this.options.indentSpaceCount);
        let elements: string[] = [];
        fields.forEach(f => {
            if (isMDMField(f)) {
                elements.push(this.serializeMDMFieldToString(f, indentLevel + 1) + ";");
            }
            else if (isMDMVariable(f)) {
                elements.push(this.serializeVariableToString(f, indentLevel + 1) + ";");
            }
        });
        return `${indent}helperfields (${this.options.lineFeedCharater}${elements.join(this.options.lineFeedCharater)}${this.options.lineFeedCharater}${indent})`;
    }

    private serializeHelperFieldsToXml(dom: XMLDocument, fields: t.Types<t.Variable>): Element {
        const element = this.serializeTypesToXml(dom, fields, "helperfields", this.serializeVariableToXml);
        element.setAttribute("id", fields.uuid);
        element.setAttribute("name", "@helperfields");
        element.setAttribute("global-name-space", "-1");
        return element;
    }

    private serializeFieldLikeToString(fieldLike: t.IMDMLabeledObject, indentLevel = 0): string {
        let original: t.MDMField | undefined;
        if (fieldLike.reference) {
            if (isMDMField(fieldLike)) {
                original = fieldLike;
            }
            fieldLike = fieldLike.reference;
        }
        if (isMDMVariable(fieldLike)) {
            return this.serializeVariableToString(fieldLike, indentLevel, original);
        }
        if (isMDMField(fieldLike)) {
            return this.serializeMDMFieldToString(fieldLike, indentLevel);
        }
        if (isMDMClass(fieldLike)) {
            return this.serializeClassToString(fieldLike, indentLevel);
        }
        if (isMDMPage(fieldLike)) {
            return this.serializePageToString(fieldLike, indentLevel);
        }
        if (isMDMCompound(fieldLike)) {
            return this.serializeCompoundToString(fieldLike, indentLevel);
        }
        if (isMDMElements(fieldLike)) {
            return this.serializeElementsToString(fieldLike, true, indentLevel);
        }
        return "";
    }

    private serializeFieldLikeToXml(obj: t.IMDMLabeledObject): Element | undefined {
        if (isMDMVariable(obj)) {
            return this.serializeVariableToXml(obj);
        }
        if (isMDMField(obj)) {
            return this.serializeMDMFieldToXml(obj);
        }
        if (isMDMClass(obj)) {
            return this.serializeClassToXml(obj);
        }
        if (isMDMPage(obj)) {
            return this.serializePageToXml(obj);
        }
        if (isMDMCompound(obj)) {
            return this.serializeCompoundToXml(obj);
        }
        if (isMDMElements(obj)) {
            return this.serializeElementsToXml(obj);
        }
        return undefined;
    }

    // DB Questions
    //
    // field_name
    //     [ "field_label" ]
    //     [ [ <properties> ] ]
    //     [ <styles and templates> ]
    // [text | double | long | date]
    // db
    // (
    //         ConnectionString = "connection_string_value",
    //         Table = "table_value",
    //         [MinAnswers = min_answers_value]
    //         [MaxAnswers = max_answers_value]
    //         [SQLFilter = sqlfilter_string]
    //         [CacheTimeout = cache_timeout_value]
    //         Columns
    //         (
    //              ID = "id_column_name",
    //              Label = "label_column_name"
    //              [KeyCode = "keycode_column_name"]
    //              [File = "file_column_name"]
    //              [AnalysisValue = "analysis_value_column_name"]
    //              [Fixed = "fixed_column_name"]
    //              [Exclusive = "exclusive_column_name"]
    //         )
    //     )
    //     [ [ range_expression ] ]
    //     [ <codes> ]
    //     [ expression ("expression_text") ]
    //     [ ( initialanswer | defaultanswer ) (date_value) ]
    //     [ <axis> ]
    //     [ <usage-type> ]
    //     [ <helperfields> ]
    //     [ nocasedata ]
    //     [ unversioned ]

    // field_name
    //     [ "field_label" ]
    //     [ [ <properties> ] ]
    //     [ <styles and templates> ]
    // loop
    //     [ range_expression ]
    // db
    // (
    //         ConnectionString = "connection_string_value",
    //         Table = "table_value",
    //         [SQLFilter = sqlfilter_string]
    //         [CacheTimeout = cache_timeout_value]
    //         [IteratorIDType = [text | double | long | date]]
    //         Columns
    //         (
    // 	            ID = "id_column_name",
    // 	            Label = "label_column_name"
    // 	            [KeyCode = "keycode_column_name"]
    // 	            [File = "file_column_name"]
    // 	            [AnalysisValue = "analysis_value_column_name"]
    // 	            [Fixed = "fixed_column_name"]
    // 	            [Exclusive = "exclusive_column_name"]
    //         )
    // )
    //     fields
    //     ( <field> (; <field> )* [;] )
    //         [ row | column ]
    //         [ noexpand ]

    private serializeMDMFieldToString(field: t.MDMField, indentLevel = 0): string {
        let inCompound = isMDMCompound(field.parent);
        // ref
        if (field.reference) {
            return this.serializeFieldLikeToString(field.reference, indentLevel);
        }
        const indent = " ".repeat(indentLevel * this.options.indentSpaceCount);
        let result = this.serializeMDMLabeledObjectToString(field, true, indentLevel);
        // normal
        if (field.iteratorType === undefined || (field.dbQuestionType !== undefined && field.dbQuestionType !== t.DBQuestionsTypes.array)) {
            // info
            if (field.dataType === t.DataTypes.none) {
                return `${result}${this.options.lineFeedCharater}${indent}info`;
            }
            result += this.options.lineFeedCharater + indent + dataTypeToString(field.dbDataType !== undefined ? field.dbDataType : field.dataType);
            // range
            if (field.rangeExpression) {
                result += ` ${field.rangeExpression}`;
            }
            else if (field.ranges) {
                result += ` ${this.serializeRangesToString(field.ranges)}`;
            }
            // DB Question
            if (field.dbQuestionProperties) {
                result += this.serializeDBQuestionPropertiesToString(field.dbQuestionProperties, indentLevel);
            }
            // elements
            if (field.elements.length > 0 && !inCompound) {
                result += this.serializeElementsToString(field.elements, false, indentLevel);
            }
            else {
                result += `${indent}{${this.options.lineFeedCharater}${indent}}`;
            }
            result += this.serializeFieldSuffixToString(field, indentLevel);
            // result += `${field.elements.length === 0 && suffixes.length > 0 ? `${GlobalConfigs.mdm.lineFeedCharater}${indent}` : (suffixes.length === 0 ? "" : " ")}${suffixes}`;
        }
        // loop [[..]] fields
        else {
            result += this.options.lineFeedCharater + indent + "loop";
            if (field.iteratorType === t.IteratorTypes.number || field.iteratorType === t.IteratorTypes.numericRanges) {
                if (field.rangeExpression) {
                    result += ` ${field.rangeExpression}`;
                }
                else if (field.ranges) {
                    result += ` ${this.serializeRangesToString(field.ranges)}`;
                }
                if (field.dbQuestionProperties) {
                    result += this.serializeDBQuestionPropertiesToString(field.dbQuestionProperties, indentLevel);
                }
            }
            else {
                result += inCompound ? `${this.options.lineFeedCharater}${indent}{${this.options.lineFeedCharater}${indent}}` : this.serializeElementsToString(field.elements, false, indentLevel);
            }
            result += ` fields ${this.serializeMDMLabeledObjectToString(field.class, false, indentLevel + 1)}${this.options.lineFeedCharater}${indent}(`;
            field.fields.forEach(field => {
                result += this.options.lineFeedCharater + this.serializeFieldLikeToString(field, indentLevel + 1) + ";";
            });
            result += `${this.options.lineFeedCharater}${indent})${field.orientation === t.DisplayOrientations.col ? " column" : field.orientation === t.DisplayOrientations.row ? " row" : ""}${field.expand ? " expand" : ""}${field.isGrid ? " grid" : ""}`;
        }
        return result;
    }

    private serializeMDMFieldToXml(field: t.MDMField, specifyTag?: string): Element {
        const dom = field.document.dom;
        let tag = specifyTag ? specifyTag : field.iteratorType !== undefined ? "loop" : "variable";
        const element = this.serializeMDMLabeledObjectToXml(field, tag, false, {
            properties: (e, r) => {
                const orientation = this.serializeDisplayOrientationToXml(field);
                if (orientation) {
                    let prop = e;
                    if (!prop) {
                        prop = dom.createElement("properties");
                        if (r) {
                            r.appendChild(prop);
                        }
                    }
                    prop.appendChild(orientation);
                }
            }
        });

        if (field.reference) {
            element.setAttribute("ref", field.reference.uuid);
            return element;
        }

        // properties
        if (field.dbQuestionProperties) {
            element.setAttribute("isdb", "-1");
            if (field.dbQuestionType !== undefined) {
                element.setAttribute("dbtype", field.dbQuestionType.toString());
            }
            if (field.dbDataType !== undefined) {
                element.setAttribute("dbdatatype", field.dbDataType.toString());
            }
        }
        element.setAttribute("type", field.dataType.toString());
        if (field.isGrid) {
            element.setAttribute("isgrid", "1");
        }
        if (field.iteratorType !== undefined) {
            element.setAttribute("iteratortype", field.iteratorType.toString());
        }
        if (field.rangeExpression) {
            element.setAttribute("rangeexp", field.rangeExpression);
        }
        if (field.autoElements) {
            element.setAttribute("autoelements", "-1");
        }
        if (field.upperBound) {
            element.setAttribute("upperbound", field.upperBound);
        }
        if (field.lowerBound) {
            element.setAttribute("lowerbound", field.lowerBound);
        }

        // elements
        element.appendChild(this.serializeElementsToXml(field.elements));

        // class
        element.appendChild(this.serializeClassToXml(field.class));

        if (field.ranges) {
            element.appendChild(this.serializeRangesToXml(dom, field.ranges));
        }
        return element;
    }

    private serializeCompoundToString(compound: t.Compound, indentLevel = 0): string {
        const indent = " ".repeat(indentLevel * this.options.indentSpaceCount);
        let result = this.serializeMDMLabeledObjectToString(compound, true, indentLevel);
        result += ` compound${this.options.lineFeedCharater}${indent}{${this.options.lineFeedCharater}${indent}} fields${this.options}${indent}(`;
        compound.fields.forEach(field => {
            result += `${this.options.lineFeedCharater}${this.serializeFieldLikeToString(field, indentLevel + 1)};`;
        });
        result += `${this.options.lineFeedCharater}${indent})${(compound.orientation === t.DisplayOrientations.col ? " column" : compound.orientation === t.DisplayOrientations.row ? " row" : "")};`;
        return result;
    }

    private serializeCompoundToXml(compound: t.Compound): Element {
        const dom = compound.document.dom;
        const element = this.serializeMDMLabeledObjectToXml(compound, "compound", false, {
            properties: (e?, r?) => {
                const orientation = this.serializeDisplayOrientationToXml(compound);
                if (orientation) {
                    let prop = e;
                    if (!prop) {
                        prop = dom.createElement("properties");
                        if (r) {
                            r.appendChild(prop);
                        }
                    }
                    prop.appendChild(orientation);
                }
            },
        });
        // types
        const types = this.serializeTypesToXml(dom, compound.types, "types", this.serializeElementsToXml);
        types.setAttribute("name", "@types");
        types.setAttribute("global-name-space", "-1");
        element.appendChild(types);
        // fields
        const fields = this.serializeTypesToXml(dom, compound.fields, "fields", this.serializeFieldLikeToXml);
        fields.setAttribute("name", "@fields");
        fields.setAttribute("global-name-space", "-1");
        element.appendChild(fields);
        // pages
        const pages = this.serializeTypesToXml(dom, compound.pages, "pages", this.serializePageToXml);
        pages.setAttribute("name", "@pages");
        pages.setAttribute("global-name-space", "-1");
        element.appendChild(pages);
        // elements
        if (compound.elements.length > 0) {
            element.appendChild(this.serializeElementsToXml(compound.elements));
        }
        return element;
    }

    private serializeVariableInstanceToXml(variableInstance: t.VariableInstance): Element {
        const dom = variableInstance.document.dom;
        const element = dom.createElement("varinstance");
        if (variableInstance.name.length > 0) {
            element.setAttribute("name", variableInstance.name);
        }
        if (variableInstance.sourceType !== undefined) {
            element.setAttribute("sourcetype", variableInstance.sourceType.toString());
        }
        element.setAttribute("variable", variableInstance.variable.uuid);
        element.setAttribute("fullname", variableInstance.fullName);
        if (variableInstance.properties.length > 0) {
            element.appendChild(this.serializePropertiesToXml(variableInstance.document.dom, variableInstance.properties));
        }
        if (variableInstance.elements) {
            element.appendChild(this.serializeMDMListToXml(variableInstance.document.dom,
                variableInstance.elements,
                "categories",
                obj => {
                    if (isMDMElementInstance(obj)) {
                        const node = variableInstance.document.dom.createElement("element");
                        if (obj.fullName) {
                            node.setAttribute("fullname", obj.fullName);
                        }
                        if (obj.properties.length > 0) {
                            node.appendChild(this.serializePropertiesToXml(dom, obj.properties));
                        }
                        return node;
                    }
                }));
        }
        return element;
    }

    private serializePageToString(page: t.Page, indentLevel = 0): string {
        const indent = " ".repeat(indentLevel * this.options.indentSpaceCount);
        let collection = page.reference && isMDMPage(page.reference) ? page.reference : page;
        let items: string[] = [];
        for (const item of collection) {
            items.push(`${this.options.lineFeedCharater}${indent}${" ".repeat(this.options.indentSpaceCount)}${item.name}`);
        }
        return `${this.serializeMDMLabeledObjectToString(page, true, indentLevel)}${this.options.lineFeedCharater}${indent}page(${items.join(",")}${this.options.lineFeedCharater}${indent})`;
    }

    private serializePageToXml(page: t.Page): Element {
        const dom = page.document.dom;
        const element = this.serializeMDMLabeledObjectToXml(page, "page", false);
        if (page.reference) {
            element.setAttribute("ref", page.reference.uuid);
            return element;
        }
        element.setAttribute("global-name-space", "0");

        function serializePageItemToXml(obj: t.Variable): Element {
            const itemNode = dom.createElement("item");
            itemNode.setAttribute("name", obj.name);
            itemNode.setAttribute("id", obj.uuid);
            if (obj.reference) {
                itemNode.setAttribute("ref", obj.reference.uuid);
            }
            return itemNode;
        }

        // deleted
        const deleted = dom.createElement("deleted");
        for (const d of page.deleted) {
            deleted.appendChild(serializePageItemToXml(d));
        }
        element.appendChild(deleted);
        for (const item of page) {
            element.appendChild(serializePageItemToXml(item));
        }
        return element;
    }

    private serializeSubAliasToXml(dom: Document, subAlias: t.SubAlias): Element {
        const element = dom.createElement("subalias");
        element.setAttribute("index", subAlias.index.toString());
        element.setAttribute("name", subAlias.name);
        return element;
    }

    private serializeNativeValueToXml(dom: Document, nativeValue: t.NativeValue): Element {
        const element = dom.createElement("nativevalue");
        element.setAttribute("fullname", nativeValue.fullName);
        element.setAttribute("value", nativeValue.value);
        return element;
    }

    private serializeNativeValuesToXml(dom: Document, nativeValues: t.NativeValues): Element {
        const element = dom.createElement("nativevalues");
        for (const native of nativeValues) {
            element.appendChild(this.serializeNativeValueToXml(dom, native));
        }
        return element;
    }

    private serializeAliasToXml(dom: Document, alias: t.Alias): Element {
        const element = dom.createElement("var");
        if (alias.properties.length > 0) {
            element.appendChild(this.serializePropertiesToXml(dom, alias.properties));
        }
        element.appendChild(this.serializeNativeValuesToXml(dom, alias.nativeValues));
        for (const subalias of alias) {
            element.appendChild(this.serializeSubAliasToXml(dom, subalias));
        }
        return element;
    }

    private serializeDataSourceToXml(dom: Document, dataSource: t.DataSource): Element {
        const element = dom.createElement("connection");
        element.setAttribute("name", dataSource.name);
        element.setAttribute("dblocation", dataSource.dbLocation);
        element.setAttribute("cdscname", dataSource.cdscName);
        element.setAttribute("project", dataSource.project);
        element.setAttribute("id", dataSource.uuid);
        if (dataSource.aliasMap && dataSource.aliasMap.length > 0) {
            for (const alias of dataSource.aliasMap) {
                element.appendChild(this.serializeAliasToXml(dom, alias));
            }
        }
        return element;
    }

    private serializeDataSourcesToXml(dom: Document, dataSources: t.DataSources): Element {
        const element = dom.createElement("datasources");
        if (dataSources.default) {
            element.setAttribute("default", dataSources.default.name);
        }
        for (const ds of dataSources) {
            element.appendChild(this.serializeDataSourceToXml(dom, ds));
        }
        return element;
    }

    private serializeAtomsToXml(dom: Document, atoms: readonly string[]): Element {
        const element = dom.createElement("atoms");
        for (const atom of atoms) {
            const atomNode = dom.createElement("atom");
            atomNode.setAttribute("name", atom);
            element.appendChild(atomNode);
        }
        return element;
    }

    private serializeUserToXml(dom: Document, user: t.User): Element {
        const element = dom.createElement("user");
        element.setAttribute("name", user.name);
        element.setAttribute("fileversion", user.fileVersion);
        element.setAttribute("comment", user.comment);
        return element;
    }

    private serializeSaveLogToXml(dom: Document, saveLog: t.SaveLog): Element {
        const element = dom.createElement("savelog");
        element.setAttribute("fileversion", saveLog.mdmFileVersion);
        element.setAttribute("versionset", saveLog.versionSet);
        element.setAttribute("username", saveLog.userName);
        element.setAttribute("date", saveLog.date.toString());
        element.setAttribute("count", saveLog.saveCount.toString());
        element.appendChild(this.serializeUserToXml(dom, saveLog.user));
        return element;
    }

    private serializeContextToXml(dom: Document, context: t.Context): Element {
        const element = dom.createElement("context");
        element.setAttribute("name", context.name);
        if (context.properties) {
            element.appendChild(this.serializePropertiesToXml(dom, context.properties));
        }
        if (context.alternatives) {
            let alternatives = this.serializeMDMListToXml(dom, context.alternatives, "alternatives", obj => {
                let altNode = dom.createElement("alternative");
                altNode.setAttribute("name", obj);
                return altNode;
            });
            element.appendChild(alternatives);
        }
        return element;
    }

    private serializeContextsToXml(dom: Document, contexts: t.Contexts, specifiedTag?: string): Element {
        const element = this.serializeMDMListToXml(dom, contexts, specifiedTag ?? "contexts", e => this.serializeContextToXml(dom, e));
        element.setAttribute("base", contexts.base);
        if (contexts.properties) {
            element.appendChild(this.serializePropertiesToXml(dom, contexts.properties));
        }
        return element;
    }

    private serializeLanguageToXml(dom: Document, language: t.Language): Element {
        const element = dom.createElement("language");
        element.setAttribute("name", language.longName);
        element.setAttribute("id", language.id);
        if (language.properties.length > 0) {
            element.appendChild(this.serializePropertiesToXml(dom, language.properties));
        }
        return element;
    }

    private serializeLanguagesToXml(dom: Document, languages: t.Languages): Element {
        const element = this.serializeMDMListToXml(dom, languages, "languages", e => this.serializeLanguageToXml(dom, e));
        element.setAttribute("base", languages.base);
        if (languages.properties) {
            element.appendChild(this.serializePropertiesToXml(dom, languages.properties));
        }
        return element;
    }

    private serializeCategoryMapToXml(dom: Document, categoryMap: t.CategoryMap): Element {
        const element = dom.createElement("categorymap");
        categoryMap.nameToValueMap.forEach((v, k) => {
            const category = dom.createElement("categoryid");
            category.setAttribute("name", k);
            category.setAttribute("value", v.toString());
            element.appendChild(category);
        });
        return element;
    }

    private serializeMappingToXml(dom: Document, mapping: readonly t.VariableInstance[]): Element {
        const element = dom.createElement("mappings");
        for (const item of mapping) {
            element.appendChild(this.serializeVariableInstanceToXml(item));
        }
        return element;
    }

    private serializeVersionToXml(dom: Document, version: t.Version): Element {
        const element = dom.createElement("version");
        element.setAttribute("name", version.name);
        element.setAttribute("label", version.label);
        element.setAttribute("description", version.description);
        element.setAttribute("createdby", version.createdBy);
        element.setAttribute("id", version.uuid);
        element.setAttribute("locked", booleanToString(version.isLocked));
        if (version.dateLocked) {
            element.setAttribute("datelocked", version.dateLocked);
        }
        if (version.major) {
            element.setAttribute("major", version.major);
        }
        if (version.minor) {
            element.setAttribute("minor", version.minor);
        }
        element.setAttribute("datecreated", version.dateCreated);
        if (version.mdmVersion) {
            element.setAttribute("mdmversion", version.mdmVersion);
        }
        return element;
    }

    public serializeToXml(document: t.BaseMDMDocument, options?: MDMSerializerOptions): Document {
        let savedOption = this.options;
        if (options) {
            this.options = options;
        }
        const doc = domImpl.createDocument(null, null);
        const root = doc.createElement("xml");
        const mdm = doc.createElement("mdm:metadata");
        // mdm
        mdm.setAttribute("mdm_createversion", document.createdByVersion);
        mdm.setAttribute("mdm_lastversion", document.lastUpdatedByVersion);
        mdm.setAttribute("id", document.uuid);
        mdm.setAttribute("data_version", document.mdmVersion);
        mdm.setAttribute("data_sub_version", "1");
        mdm.setAttribute("systemvariable", "1");
        mdm.setAttribute("dbfiltervalidation", "-1");
        mdm.setAttribute("xmlns:mdm", "http://www.spss.com/mr/dm/metadatamodel/Arc 3/2000-02-04");

        // datasource
        mdm.appendChild(this.serializeDataSourcesToXml(doc, document.dataSources));
        // properties
        mdm.appendChild(this.serializePropertiesToXml(doc, document.properties));
        // templates
        mdm.appendChild(this.serializeTemplatesToXml(doc, document.templates));
        // labels
        this.serializeLabelsToXml(mdm, document.labels);
        // definition
        const definition = doc.createElement("definition");
        document.definitions.forEach(v => {
            let fieldLike = this.serializeFieldLikeToXml(v);
            if (fieldLike) {
                definition.appendChild(fieldLike);
            }
        });
        mdm.appendChild(definition);
        // class - system
        const system = this.serializeTypesToXml(doc, document.systemVariables, "system", this.serializeClassToXml);
        system.setAttribute("name", "@fields");
        system.setAttribute("global-name-space", "-1");
        mdm.appendChild(system);
        // systemrouting
        mdm.appendChild(this.serializeRoutingsToXml(doc, document.systemRouting, "systemrouting"));
        // mapping
        mdm.appendChild(this.serializeMappingToXml(doc, document.mapping));
        // design
        const design = doc.createElement("design");
        // design - fields
        const fields = this.serializeTypesToXml(doc, document.fields, "fields", this.serializeFieldLikeToXml);
        fields.setAttribute("name", "@fields");
        fields.setAttribute("global-name-space", "-1");
        design.appendChild(fields);
        // design - types
        const types = this.serializeTypesToXml(doc, document.types, "types", this.serializeElementsToXml);
        types.setAttribute("name", "@types");
        types.setAttribute("global-name-space", "-1");
        design.appendChild(types);
        // design - pages
        const pages = this.serializeTypesToXml(doc, document.pages, "pages", this.serializePageToXml);
        pages.setAttribute("name", "@pages");
        pages.setAttribute("global-name-space", "-1");
        design.appendChild(pages);
        // design - routings
        design.appendChild(this.serializeRoutingsToXml(doc, document.routings));
        // design - properties
        design.appendChild(this.serializePropertiesToXml(doc, document.properties));
        mdm.appendChild(design);
        // languages
        mdm.appendChild(this.serializeLanguagesToXml(doc, document.languages));
        // contexts
        mdm.appendChild(this.serializeContextsToXml(doc, document.contexts));
        // labeltypes
        mdm.appendChild(this.serializeContextsToXml(doc, document.labelTypes, "labeltypes"));
        // routingcontexts
        mdm.appendChild(this.serializeContextsToXml(doc, document.routingContexts, "routingcontexts"));
        // scripttypes
        mdm.appendChild(this.serializeContextsToXml(doc, document.scriptTypes, "scripttypes"));
        // savelogs
        const saveLogs = doc.createElement("savelogs");
        for (const log of document.saveLogs) {
            saveLogs.appendChild(this.serializeSaveLogToXml(doc, log));
        }
        mdm.appendChild(saveLogs);
        // atoms
        mdm.appendChild(this.serializeAtomsToXml(doc, document.atoms));
        // versionlist
        const versionList = doc.createElement("versionlist");
        for (const version of document.versions) {
            versionList.appendChild(this.serializeVersionToXml(doc, version));
        }
        mdm.appendChild(versionList);
        // categorymap
        mdm.appendChild(this.serializeCategoryMapToXml(doc, document.categoryMap));

        root.appendChild(mdm);
        doc.appendChild(root);

        if (options) {
            this.options = savedOption;
        }

        return doc;
    }

    public serializeToString(document: t.BaseMDMDocument, options?: MDMSerializerOptions): string {
        this.options = options ?? this.options;
        const items: string[] = [];
        // Hdata
        if (document.properties.countItem(document.contexts.current, e => e.length) > 0 ||
            document.templates.countItem(document.contexts.current, e => e.length) > 0 ||
            document.styles.countItem(document.contexts.current, _ => 1) > 0 ||
            document.labelStyles.countItem(document.contexts.current, _ => 1)) {
            items.push(`${this.options.lineFeedCharater}${this.serializeMDMLabeledObjectToString(document, true, 1, () => "HDATA")};`);
        }
        // Types
        for (const [, item] of document.types) {
            if (isMDMDocument(item.parent)) {
                items.push(`${this.options.lineFeedCharater}${this.serializeElementsToString(item, true, 1)};`);
            }
        }
        // Fields
        for (const [, field] of document.fields) {
            items.push(`${this.options.lineFeedCharater}${this.serializeFieldLikeToString(field, 1)};`);
        }
        // Page
        for (const [, page] of document.pages) {
            items.push(`${this.options.lineFeedCharater}${this.serializePageToString(page, 1)};`);
        }
        return `Metadata(${document.languages.current}, ${document.contexts.current}, ${document.labelTypes.current})${this.options.lineFeedCharater}${items.join(this.options.lineFeedCharater)}${this.options.lineFeedCharater.repeat(2)}End Metadata`;
    }

}

